from sympy import simplify, expand, solveset
from sympy.sets.fancysets import Complexes, Reals
from sympy.core.relational import Eq
from sympy.core.expr import Expr
from sympy.core.symbol import _symbol, uniquely_named_symbol
from sympy.core.numbers import Rational
from sympy.geometry.util import find
from sympy.geometry.point import Point2D
from sympy.matrices.dense import Matrix
from sympy.solvers.solveset import linsolve


class QuadraticFunction:
    # ToDo:
    ######
    # 这里要保证输入的表达式一定是二次函数，有些时候是可以退化成一次函数甚至是常数表达式的。
    ######
    # 在具体的实际使用过程中，很多时候是已经有了二次函数的表达式，
    # 如果还要重新输入二次函数的系数的话，反而麻烦了。
    #
    # 所以借鉴sympy里对Line的定义，重新整理一下。
    """An univariate quadratic function class. it can use to solve all the '$ ax^2+bx+c $' functions problem.

    Parameters
    ==========

    f : Expr, optional
    a,b,c : number or SymPy expression, Sympy Point2D, optional
      "f" or "a,b,c" must be supplied to create a QuadraticFunction class.

    See Also
    ========

    sympy.poly.Poly

    Examples
    ========

    >>> from sympy.abc import x, y
    >>> from sympy.geometry.point import Point
    >>> q1=QuadraticFunction(1,2,1)
    >>> q1.equation()
    x**2 + 2*x + 1
    >>> q1.equation(y)
    y**2 + 2*y + 1
    >>> q2=QuadraticFunction(2*y**2+3*y+3,x=y)
    >>> q2.a
    2
    >>> q2.b
    3
    >>> q2.c
    3
    >>> q3 = QuadraticFunction(Point(0,20),Point(5,39),Point(10,48))
    >>> q3.equation()
    -x**2/5 + 24*x/5 + 20
    """

    def __init__(self, *args, **kwargs):
        if len(args) == 1 and isinstance(args[0], (Expr, Eq)):
            equation = args[0]
            if isinstance(equation, Eq):
                equation = equation.lhs - equation.rhs

            missing = uniquely_named_symbol("?", args)
            if not kwargs:
                free_symbol_list = list(equation.free_symbols)
                if len(free_symbol_list) == 1:
                    x = free_symbol_list[0]
                else:
                    x = "x"
            else:
                x = kwargs.pop("x", missing)
            if kwargs:
                raise ValueError("expecting only x as keywords")

            def find_or_missing(x):
                try:
                    return find(x, equation)
                except ValueError:
                    # return missing
                    raise ValueError("not find the symbol.")

            x = find_or_missing(x)

            a, b, c = [expand(equation).coeff(x, i) for i in reversed(range(3))]
            # check the equation is quadratic function.
            check = a * x**2 + b * x + c - equation
            if simplify(check) != 0:
                raise ValueError("The input isn't quadratic function of ", x, ".")
        elif len(args) == 3:
            points = list(filter(lambda n: isinstance(n, Point2D), args))
            if len(points) == 3:
                A = Matrix(3, 3, lambda i, j: (points[i].x) ** (2 - j))
                B = Matrix(3, 1, lambda i, j: points[i].y)
                a, b, c = list(linsolve((A, B)))[0]
            else:
                a, b, c = args
        else:
            raise ValueError("The parameter or parameters' length is wrong.")

        self.a = a
        self.b = b
        self.c = c

    @property
    def symmetry(self):
        """The symmetry point value of the quadratic function.

        Returns
        =======

        symmetry : number or SymPy expression

        Examples
        ========

        >>> q1 = QuadraticFunction(1, 2, 1)
        >>> q1.symmetry
        -1.0
        >>> from sympy import symbols
        >>> a,b,c = symbols("a,b,c",real=True)
        >>> q2 = QuadraticFunction(a,b,c)
        >>> q2.symmetry
        -b/(2*a)

        """
        return -self.b / (2 * self.a)

    @property
    def delta(self):
        """The discriminant of the quadratic function.

        See Also
        ========

        sympy.polys.polytools.discriminant

        Returns
        =======

        delta : number or SymPy expression

        Examples
        ========

        >>> q1 = QuadraticFunction(1, 3, 1)
        >>> q1.delta
        5
        >>> from sympy.abc import a,b,c
        >>> q2 = QuadraticFunction(a,b,c)
        >>> q2.delta
        -4*a*c + b**2

        """
        return (self.b) ** 2 - 4 * self.a * self.c

    @property
    def extreme_value(self):
        """The extreme value of the quadratic function

        Returns
        =======

        extreme_value : number or SymPy expression

        Examples
        ========

        >>> from sympy import Rational
        >>> q1 = QuadraticFunction(1, 3, 2)
        >>> q1.extreme_value
        -0.250000000000000
        >>> Rational(q1.extreme_value)
        -1/4
        >>> from sympy.abc import a,b,c
        >>> q2 = QuadraticFunction(a,b,c)
        >>> q2.extreme_value
        c - b**2/(4*a)

        """
        return simplify(expand(-self.delta / (4 * self.a)))

    @property
    def roots(self):
        """The roots of the equation `` ax^2 + bx + c = 0 ``. If the domains is left, it will only return real roots.

        Parameters
        ==========

        domains : Reals, Integer, Complexes, Naturals, ..., optional

        Returns
        =======

        roots : Set

        Examples
        ========

        >>> from sympy.sets.sets import FiniteSet, EmptySet
        >>> q1 = QuadraticFunction(1, 3, 2)
        >>> q1.roots
        {-2, -1}
        >>> q2 = QuadraticFunction(1, 1, 1)
        >>> q2.roots
        EmptySet

        """
        x = _symbol("x")
        return solveset(self.equation(x), x)

    @property
    def vertex(self):
        return Point2D(self.symmetry, self.extreme_value)

    def equation(self, z="x"):
        """The quadratic function's equation.

        Parameters
        ==========

        z : Symbol or str, optional

        Returns
        =======

        equation : SymPy expression

        Examples
        ========

        >>> q1 = QuadraticFunction(1, 2, 1)
        >>> q1.equation()
        x**2 + 2*x + 1
        >>> q1.equation("y")
        y**2 + 2*y + 1
        >>> from sympy.abc import t,a,b,c
        >>> q1.equation(t)
        t**2 + 2*t + 1
        >>> q2 = QuadraticFunction(a,b,c)
        >>> q2.equation(t)
        a*t**2 + b*t + c

        """
        z = _symbol(z)
        return self.a * z**2 + self.b * z + self.c

    def symmetry_equation(self, z="x", *args, **kwargs):
        """The symmetry expression of the quadratic function.

        Parameters
        ==========

        z : Symbol or str, optional

        Returns
        =======

        equation : SymPy expression

        Examples
        ========

        >>> q1 = QuadraticFunction(1, 2, 1)
        >>> q1.symmetry_equation()
        (x + 1.0)**2
        >>> q1.symmetry_equation("y")
        (y + 1.0)**2
        >>> from sympy.abc import t,a,b,c
        >>> from sympy import S
        >>> q2 = QuadraticFunction(1, S(2), 1)
        >>> q2.symmetry_equation(t)
        (t + 1)**2
        >>> q3 = QuadraticFunction(a,b,c)
        >>> q3.symmetry_equation(t)
        a*(t + b/(2*a))**2 + c - b**2/(4*a)

        """
        z = _symbol(z, real=True)
        if kwargs.get("rational") == True:
            return self.a * (z - Rational(self.symmetry)) ** 2 + self.extreme_value
        else:
            return self.a * (z - self.symmetry) ** 2 + self.extreme_value


"""

# below is test.
# q=QuadraticFunction(1,2,1)
# print(q.a)
# print(q.to_expr())

Everything is improving. the ups is written on early times.

"""
